# -*- coding: utf-8 -*-

import numpy as np
from collections import defaultdict
from collections import Counter
from itertools import combinations

# 推荐模块
class GetRecommendation:
    
    def __init__(self, N = 100):
        self.N = N                     # Top N

    def fit(self, user_item, item_count = None, **kwargs):
        '''
        train_dic: 用户物品对
        item_set: 物品集合
        '''
        self.user_item = user_item
        self.item_count = item_count
        
class RandomCF(GetRecommendation):
    
    def fit(self, user_item, item_count, **kwargs):
        '''
        生成物品集合
        '''
        self.item_set = list(item_count.keys())

    def predict(self, user):
        '''
        随机推荐
        '''
        items = np.random.choice(self.item_set, self.N, replace = False) 
        rank = dict.fromkeys(items, 0)
        return rank

class PopularityCF(GetRecommendation):
    
    def get_item_popularity(self):
        '''
        计算物品流行度
        '''
        self.item_count_list  = sorted(self.item_count.items(), key = lambda x:x[1], reverse = True)
    
    def fit(self, user_item, item_count, **kwargs):
        '''
        对物品进行计数
        '''
        self.item_count = item_count
        self.get_item_popularity()
        
    def predict(self, user):
        '''
        推荐热门产品
        '''
        rank = {self.item_count_list[i][0]:0 for i in range(min(len(self.item_count_list), self.N))}
        return rank
    
class UserCF(PopularityCF):

    def __init__(self, N):
        self.N = N
        self.item_count_list = None   # 增加物品流行度

    def com_similarity(self, item_user):
        '''
        相似度组合
        '''
        # 用户相似度
        self.similarity = defaultdict(float)
        for item in item_user:
            for key in combinations(item_user[item], 2):
                self.similarity[key] += 1
        for key in self.similarity:
            self.similarity[key] /= np.sqrt(len(self.user_item[key[0]]) * len(self.user_item[key[1]]))

    def single_similarity(self, user):
        '''返回和user相似的用户列表'''
        res = {}
        for key, value in self.similarity.items():
            if key[0] == user:
                res[key[1]] = value
            elif key[1] == user:
                res[key[0]] = value
            else:
                continue
        return res

    def fit(self, user_item, item_count = None, item_user = None, **kwargs):
        '''
        计算用户相似度和物品流行度
        user_item: 用户: {物品}， item_user: 物品: {用户}
        '''
        self.user_item = user_item
        self.item_count = item_count
        if self.item_count is not None:
            self.get_item_popularity()
        # 计算用户相似度
        self.com_similarity(item_user)
        
    def predict(self, user):
        '''
        基于用户的协同过滤
        '''
        W = self.single_similarity(user)
        # 如果user在训练集中, 获取已经访问过的物品
        interacted_items = self.user_item.get(user, set())
        rank = defaultdict(float)
        for v, wu in sorted(W.items(), key = lambda x:x[1], reverse = True)[:self.N]:
            for item in self.user_item[v]:
                if item in interacted_items:
                    continue
                else:
                    rank[item] += wu   # 可能会有多人推荐
        # 如果推荐物品数量不足N个，使用流行产品代替
        len_ = len(rank)
        if len_ < self.N and self.item_count_list is not None:
            for i in range(min(self.N - len_, len(self.item_count_list))):
                rank[self.item_count_list[i][0]] = 0   # 概率设为 0
        #print("UserCF", rank)
        rank = dict(sorted(rank.items(), key = lambda x:x[1], reverse = True)[:self.N])
        return rank
    
    
# 带惩罚项的用户协同过滤
class UserCFIUF(UserCF):
    
    def com_similarity(self, item_user):
        '''
        相似度组合
        '''
        #print("UserII被调用了!")
        # 用户相似度
        self.similarity = defaultdict(float)
        for item in item_user:
            for key in combinations(item_user[item], 2):
                self.similarity[key] += 1 / np.log(1 + len(item_user[item]))  # 增加惩罚项
        for key in self.similarity:
            self.similarity[key] /= np.sqrt(len(self.user_item[key[0]]) * len(self.user_item[key[1]]))
    

# 基于物品的协同过滤
class ItemCF(UserCF):
    
     def fit(self, user_item, item_count = None, item_user = None, **kwargs):
        '''
        计算用户相似度和物品流行度
        user_item: 用户: {物品}， item_user: 物品: {用户}
        '''
        #print("ItemCF被调用了!")
        self.user_item = item_user      #  基于物品的要将物品
        self.item_count = item_count
        if self.item_count is not None:
            self.get_item_popularity()
        # 计算物品相似度
        self.com_similarity(user_item)
        
# 带惩罚项的基于物品的协同过滤
class ItemCFIIF(UserCFIUF, ItemCF):
    
     def f(self):
        pass
    
    
# 归一化的ItemCF
class ItemCFNorm(ItemCF):
    
    def single_similarity(self, user):
        '''返回和user相似的用户列表'''
        res = {}
        for key, value in self.similarity.items():
            if key[0] == user:
                res[key[1]] = value
            elif key[1] == user:
                res[key[0]] = value
            else:
                continue
        if res:
            max_ = max(res.values())
            res = {key:value/max_ for key,value in res.items()}
        return res
    
# 哈利波特问题
class ItemCF_alpha(ItemCF):
    
    def __init__(self, N, alpha = 0.5):
        '''
         增加流行度惩罚
        '''
        super().__init__(N)
        self.alpha = alpha
        
    def com_similarity(self, item_user):
        '''
        相似度组合
        '''
        # 用户相似度
        self.similarity = defaultdict(float)
        for item in item_user:
            for key in combinations(item_user[item], 2):
                self.similarity[key] += 1
        for key in self.similarity:
            n_i, n_j = len(self.user_item[key[0]]), len(self.user_item[key[1]])
            if n_i > n_j:   # 确保n_j是热门产品
                n_i, n_j = n_j, n_i
            self.similarity[key] /= (n_i ** (1 - self.alpha) * n_j ** self.alpha)
            
            
# 隐语义模型
class LatenFactorModel:

    def __init__(self, N = 10, user_nums = 200, item_nums = 400, F = 100, max_iters = 100, alpha_ = 0.02,
                 lambda_ = 0.01, radio = 1, items_pool = None):
        # 用户数量 物品数量
        self.user_nums = user_nums
        self.item_nums= item_nums
        self.F = F
        self.P = np.random.randn(user_nums, self.F)   # 用户向量
        self.Q = np.random.randn(self.F, item_nums)   # 物品向量
        self.max_iters = max_iters
        self.alpha_ = alpha_
        self.lambda_ = lambda_
        self.radio = radio
        self.N = N
        self.items_pool = items_pool
        
    def RandomSelectNegativeSample(self, user_dic):
        '''
        针对一个用户而言
        '''
        ret = dict.fromkeys(user_dic, 1)
        items = list(set(self.items_pool) - set(ret.keys()))
        negative_items = np.random.choice(items, min(int(len(ret) * self.radio), len(items)), replace = False)
        ret.update(dict.fromkeys(negative_items, 0))
        return ret
        
    def fit(self, user_item):
        '''
        传入用户:{物品}
        '''
        self.user_item = user_item
        for step in range(self.max_iters):
            for user, items in user_item.items():
                samples = self.RandomSelectNegativeSample(self.user_item[user])
                for item, rui in samples.items():
                    eui = rui - 1 / (1 + np.exp(-self.P[user,:] @ self.Q[:,item]))
                    self.P[user,:] += self.alpha_ * (eui * self.Q[:,item] - self.lambda_ * self.P[user,:])
                    self.Q[:,item] += self.alpha_ * (eui * self.P[user,:] - self.lambda_ * self.Q[:,item])
            self.alpha_ *= 0.9
    
    def predict(self, user):
        '''Top N'''
        tu = self.user_item.get(user, set())
        rank = dict(zip(range(self.item_nums), self.P[user] @ self.Q))    # M x 1
        # 去除已经有过的物品
        for i in tu:
            rank.pop(i)
        rank = sorted(rank.items(), key = lambda x:x[1], reverse = True)[:self.N]
        return dict(rank)

# 基于标签的推荐算法
# 推荐模块
class LabelRecommed:
    
    def __init__(self, N):
        self.N = N
    
    def countLabel(self, u_l = [0,2]):
        '''
        record: 三元组数据集
        u_1: [0,2] 表示user,label [2, 1]表示label,item
        '''
        # 用户常用标签
        user_tags = {}
        for user,label in self.record[:,u_l]:
            if user not in user_tags:
                user_tags[user] = defaultdict(int)
            user_tags[user][label] += 1
        return user_tags
    
    def fit(self, record):
        self.record = record
        self.user_tags = self.countLabel()
        self.item_tags = self.countLabel([1, 2])
        self.tag_items = self.countLabel([2, 1])
        self.user_items = self.countLabel([0, 1])
        
    def predict(self, user):
        rank = defaultdict(float)
        # 已经被打过标签的物品
        tagged_items = self.user_items[user]
        for tag, wut in self.user_tags[user].items():
            target = set(self.tag_items[tag].keys()) - set(tagged_items)
            for item in target:
                rank[item] += wut * self.tag_items[tag][item]

        rank = sorted(rank.items(), key = lambda x:x[1], reverse = True)[:self.N]
        return dict(rank)